package utwente.hmi.touchmol;

import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;

import javax.swing.JFrame;

import org.jmol.api.JmolStatusListener;

import utwente.hmi.touchmol.elements.*;

import com.merl.diamondtouch.*;
import com.merl.diamondtouch.events.*;

/**
 * Main class for TouchMol. It is used to start the application, process gesture input,
 * and to serve as a basis for turn-taking GUI extensions. 
 * @author Jeroen Logtenberg
 * Base code used from JmolWii by Wim Fikkert
 */
public abstract class TouchMolGUI extends JFrame implements KeyListener, PropertyChangeListener, 
				DTTouchListener, JmolStatusListener, FocusListener { 
	private static final long serialVersionUID = -8869404014994953891L;

	
	//Screen, table and finger dimension.
	//public final static int SCREEN_WIDTH = 1280;
	//public final static int SCREEN_HEIGHT = 1024;
	public final static int TABLE_COLS = 2752;
	public final static int TABLE_ROWS = 2064;
	public final static int FINGER_SIZE = 70; //Defines the maximum x or y dimension for a finger tip.
		
	//Display panels
	MolPanel jmolView;
	ScreenshotPanel screens;
	int displayWidth, displayHeight;
	int SCALE;
	static boolean FULLSCREEN;
	
	//Input devices
	private DTReader dtReader;
	private Robot robot;
	private static Configuration conf;
	
	//Users
	public static final int USER_RED = 0;
	public static final int USER_BLUE = 1;
	public static final int USER_GREEN = 2;
	public static final int USER_YELLOW = 3;
	
	//Turn-taking protocols
	public static final int TT_NONE = 4000;
	public static final int TT_PAUSE_DETECTION = 4001;
	public static final int TT_EXPLICIT = 4002;
	public static final int TT_MODERATOR = 4003;
	public static final int TT_QUEUEMODERATOR = 4004;
	
	//Turn-taking settings
	//private final int moderator = 0; //Specifies the user that has moderator status [0-4].
	static long pauseDelay; //Specifies the time delay for the pause detection protocol.
	public static int turntakingProtocol;
	static int currentTurn = -1; //The ID of the user who has the turn.
	
	//Gesture modes
	public static final int MODE_UNKNOWN = 5000;
	public static final int MODE_ROTATING = 5001;
	public static final int MODE_ZOOMING_TWISTING = 5002;
	public static final int MODE_TRANSLATING = 5003;
	private int[] currentMode = new int[5]; //Stores the current mode each user is in.
	
	//Touch modes
	public static final int TOUCH_UNKNOWN = 5999; //Nothing is happening, or just tapping.
	public static final int TOUCH_ONE_FINGER = 6000;
	public static final int TOUCH_TWO_FINGERS = 6001;
	public static final int TOUCH_MORE_FINGERS = 6002; //More than two finger sized segments.
	public static final int TOUCH_NON_FINGER = 6003; //Hand or fist.
	
	//Touch locations
	public static final int LOCATION_MOLPANEL = 7000;
	public static final int LOCATION_SCREENSHOTPANEL = 7001;
	int touchLocation; //The current location the touch is taking place. Updated at touchdown.
	
	//History tracking
	private DtlibInputTframe[] previousFrame = new DtlibInputTframe[5]; //Stores the previous bounding boxes of each user.
	private int previousUser = -1;
	private boolean[] maybeDoubletap = new boolean[5]; //Keeps track of doubletap flags.
	private Point[] previousTapLoc = new Point[5]; //Keeps track of where the last tap of the user was.
	private long[] previousTapTime = new long[5]; //Keeps track of when the last tap of the user was, only for doubletapping.
	private static long touchReleaseTime; //Time when the last known touch of the turn was released.
	
	//Status booleans.
	private boolean doubleTap = false; //This boolean prevents measurement mode from becoming active when double tapping.
	private boolean rightClicked = false; //Stores if a right click gesture has been made. 
	
	//Bounding box centers, stored globally for startGesture(int).
	private int centerX; 
	private int centerY;

	//Molecules
	static String pdb2f8s = "res/2F8S.pdb";
	static String pdboxycodone = "res/oxycodone.pdb";
	static String pdb1a3n = "res/1a3n.pdb";
	static String pdb1amk = "res/1AMK.pdb";
	static String molaspartame = "res/aspartame.mol";
	static String pdb2p3d = "res/2P3D.pdb";
	
	static String pdb1oy3 = "res/1OY3.pdb"; //p65 molecule, leader scenario.
	static String pdb1e3g = "res/1E3G.pdb"; //androgen receptor, leader scenario.
	static String pdb2rmn = "res/2RMN.pdb"; //p63 molecule, leader scenario.
	static String pdb6pax = "res/6PAX.pdb"; //pax-6 dna molecule, leader scenario.
	
	static String PDBfile;

	public TouchMolGUI() {
		
		try {robot = new Robot();} 
		catch (AWTException e) {e.printStackTrace();}
		
		initUsers();
		setDisplay();
		startDiamondTouch();
	}

	/**
	 * Contructs a new TouchMol application, using TouchMol.xml as config.
	 * @param args No command line arguments are parsed.
	 */
	public static void main(String args[]) {
		
		conf = new Configuration();
		
		FULLSCREEN = conf.getConfigBoolean("fullscreen");
		PDBfile = conf.getConfigValue("molecule");
		
		turntakingProtocol = conf.getConfigInteger("turntaking.protocol");
		if(turntakingProtocol == 0){
			turntakingProtocol = TT_NONE;
		}

		java.awt.EventQueue.invokeLater(new Runnable() {
			public void run() {
				//new TouchMolGUI().setVisible(true);
				switch(turntakingProtocol){
				case TT_NONE: pauseDelay = 0L;
					new DefaultGUI().setVisible(true); 
					break;
				case TT_PAUSE_DETECTION: pauseDelay = conf.getConfigInteger("pausedetection.delay");
					new DefaultGUI().setVisible(true); 
					break;
				case TT_MODERATOR: currentTurn = USER_BLUE; 
					new ModeratorGUI().setVisible(true); 
					break;
				case TT_EXPLICIT: currentTurn = -1; 
					new ExplicitGUI().setVisible(true);
					break;
				case TT_QUEUEMODERATOR: currentTurn = USER_BLUE;
					new QueueModeratorGUI().setVisible(true);
					break;
				}
				
			}
		});
	}

	//=====================================
	//Begin init methods
    //=====================================
	
	private void startDiamondTouch() {
		dtReader = new DTReader(this);
        dtReader.start();
    }

    private void initUsers(){
    	
    	touchReleaseTime = 0L;
    	
		currentMode[0] = MODE_UNKNOWN;
		currentMode[1] = MODE_UNKNOWN;
		currentMode[2] = MODE_UNKNOWN;
		currentMode[3] = MODE_UNKNOWN;
		currentMode[4] = MODE_UNKNOWN;
		
		maybeDoubletap[0] = false;
		maybeDoubletap[1] = false;
		maybeDoubletap[2] = false;
		maybeDoubletap[3] = false;
		maybeDoubletap[4] = false;
		
		previousTapTime[0] = 0L;
		previousTapTime[1] = 0L;
		previousTapTime[2] = 0L;
		previousTapTime[3] = 0L;
		previousTapTime[4] = 0L;
		
    }
    
	private void setDisplay() {
		GraphicsDevice graphicsDevice = GraphicsEnvironment
				.getLocalGraphicsEnvironment().getDefaultScreenDevice();

		if (FULLSCREEN) {
			setUndecorated(true);
			displayWidth = graphicsDevice.getDisplayMode().getWidth();
			displayHeight = graphicsDevice.getDisplayMode().getHeight();
		} else {
			displayWidth = 800;
			displayHeight = 600;
		}
		
		makeLayout(); //Layout depends on turntaking style, therefore abstract method.
		
		if (!PDBfile.equals(""))
			jmolView.loadPDB(PDBfile);
		
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		addKeyListener(this);
		addFocusListener(this);
		jmolView.addJmolStatusListener(this);
		
		if (FULLSCREEN) {
			setResizable(false);
			graphicsDevice.setFullScreenWindow(this);
		} else {
			setSize(new Dimension(displayWidth, displayHeight));
		}

		setTitle("TouchMol");
		
		pack();
		setVisible(true);
		requestFocus();
		
	}
	
	/**
	 * Constructs the layout of the application. This is different for 
	 * each turntaking solution, therefore abstract.
	 */
	abstract void makeLayout();
	
	//=====================================
	//End init methods
    //=====================================
	
	//=====================================
	//Begin gesture classification methods
    //=====================================
	
	/**
	 * Finds out which touch is currently being made on the table.
	 * @param dtInputFrame The snapshot of the table.
	 * @return TOUCH_NON_FINGER, TOUCH_ONE_FINGER, TOUCH_TWO_FINGERS, TOUCH_MORE_FINGERS or TOUCH_UNKNOWN
	 */
	public int getTouchType(DtlibInputTframe dtInputFrame){
		
		int xSegments = dtInputFrame.getXSegments().length;
		int ySegments = dtInputFrame.getYSegments().length;
		
		//Start off by filtering non-finger touches immediately.
		if(!allFingers(dtInputFrame))
			return TOUCH_NON_FINGER;
		//Then catch the single finger touch.
		else if(xSegments == 1 && ySegments == 1)
			return TOUCH_ONE_FINGER;
		//Check for two fingers, either ordinary case or two fingers on a straight line.
		else if(xSegments <= 2 && ySegments <= 2)
			return TOUCH_TWO_FINGERS;
		//This case also catches multiple fingers on a line.
		else if(xSegments >= 1 && ySegments >= 1) 
			return TOUCH_MORE_FINGERS;
		//Catch rest, should not occur.
		else {
			debug("Unknown Touch");
			return TOUCH_UNKNOWN;
		}
	}
	
	/**
	 * Checks all x and y segments in an inputframe to see
	 * if they are all smaller than FINGER_SIZE.
	 * @param dtInputFrame The InputTFrame to be checked.
	 * @return Returns true if there are no segments larger than a finger.
	 */
	private boolean allFingers(DtlibInputTframe dtInputFrame){
		DtlibSegment[] xSegs = dtInputFrame.getXSegments();
		DtlibSegment[] ySegs = dtInputFrame.getYSegments();
		int segSize;
		
		//Check X segments for larger segments than FINGER_SIZE
		for(int i = 0; i < xSegs.length; i++){
			segSize = Math.abs(xSegs[i].startPos - xSegs[i].stopPos);
			if(segSize > FINGER_SIZE)
				return false;
		}
		
		//Check Y segments for larger segments than FINGER_SIZE
		for(int i = 0; i < ySegs.length; i++){
			segSize = Math.abs(ySegs[i].startPos - ySegs[i].stopPos);
			if(segSize > FINGER_SIZE)
				return false;
		}
		//No segments are larger than a finger, return true.
		return true;
	}
	
	/**
	 * Checks if there is just one touch segment, that is larger than a finger.
	 * @param dtInputFrame The frame to process
	 * @return Whether there is only a single, non-finger segment in the frame.
	 */
	private boolean singleNonFingerSegment(DtlibInputTframe dtInputFrame){
		DtlibSegment[] xSegs = dtInputFrame.getXSegments();
		DtlibSegment[] ySegs = dtInputFrame.getYSegments();
		//debug("Non-finger check: " + xSegs.length + " x segments, " + ySegs.length + " y segments");
		if(xSegs.length == 1 && ySegs.length == 1){
			int segSizeX = Math.abs(xSegs[0].startPos - ySegs[0].stopPos);
			int segSizeY = Math.abs(ySegs[0].startPos - ySegs[0].stopPos);
			//debug("X size " + segSizeX + ", Y size " + segSizeY);
			return segSizeX > FINGER_SIZE || segSizeY > FINGER_SIZE;
		}
		return false;	
	}
	
	/**
	 * Finds out in which part of the screen the user is touching. Input in user panels 
	 * is handled in the implementing class, molpanel and bookmark panel input is handled in
	 * TouchMolGUI.
	 * @param dtInputFrame The touch input
	 * @return Returns in which panel the center of the touch bounding box is and updates touchLocation.
	 */
	abstract int determineLocation(int userID, DtlibInputTframe dtInputFrame);
	
	/**
	 * Checks if there is a screenshot located at the center of the current bounding box.
	 * @return The screenshot if there is one at the center, null otherwise.
	 */
	private Screenshot touchedScreenshot(){
		
		LinkedList<Screenshot> screenshots = screens.getScreenshots();
		Iterator<Screenshot> it = screenshots.iterator();
		Screenshot shot = null;
		while(it.hasNext()){
			shot = it.next();
			if(shot.clickOnMe(new Point(centerX-jmolView.getWidth()-2, centerY))){
				return shot;
			}
		}
		return null;
	}
	
	//=====================================
	//End gesture classification methods
    //=====================================
	
	//=====================================
	//Begin gesture processing methods
    //=====================================
	
	/**
	 * This method instructs the robot to start a certain gesture.
	 * @param gestureMode MODE_ROTATING, MODE_ZOOMING_TWISTING and MODE_TRANSLATING are currently supported.
	 */
	private void startGesture(int gestureMode){
		switch(gestureMode){
		case MODE_ROTATING:
			robot.mouseMove(centerX, centerY);
			robot.mousePress(InputEvent.BUTTON1_MASK);
			break;
		case MODE_ZOOMING_TWISTING:
			//This check disables context menu gesture on the screenshot panel.
			if(touchLocation == LOCATION_MOLPANEL){
				robot.mouseMove(centerX, centerY);
				robot.keyPress(KeyEvent.VK_SHIFT);
				robot.mousePress(InputEvent.BUTTON1_MASK);
			}
			break;
		case MODE_TRANSLATING:
			robot.mouseMove(centerX, centerY);
			robot.keyPress(KeyEvent.VK_ALT);
			robot.keyPress(KeyEvent.VK_CONTROL);
			robot.mousePress(InputEvent.BUTTON1_MASK);
			break;
		default: debug("Cannot start gesture, invalid gesture mode " + gestureMode);
		}
		
	}
	
	/**
	 * This method cleans up after performing a gesture, 
	 * by releasing keys and buttons.
	 * @param gestureMode MODE_ROTATING, MODE_ZOOMING_TWISTING and MODE_TRANSLATING are currently supported.
	 */
	private void stopGesture(int gestureMode){
		switch(gestureMode){
		case MODE_UNKNOWN:
			debug("Fast tap.");
		case MODE_ROTATING:
			robot.mouseRelease(InputEvent.BUTTON1_MASK);
			break;
		case MODE_ZOOMING_TWISTING:
			robot.keyRelease(KeyEvent.VK_SHIFT);
			robot.mouseRelease(InputEvent.BUTTON1_MASK);
			break;
		case MODE_TRANSLATING:
			robot.keyRelease(KeyEvent.VK_ALT);
			robot.keyRelease(KeyEvent.VK_CONTROL);
			robot.mouseRelease(InputEvent.BUTTON1_MASK);
			break;
		default: debug("Cannot stop gesture, invalid gesture mode: " + gestureMode);
		}
	}
	
	private void resetMolecule(){
		//debug("Resetting.");
    	scriptInput("measure OFF");
    	scriptInput("echo OFF");
		scriptInput("centerAt BOUNDBOX");
		scriptInput("zoom 100");
	}
	
	/**
	 * Constructs a rectangle based on an input frame. 
	 * The size of the bounding box of dtInputFrame is in pixels.
	 * @return The rectangle that represents dtInputFrame.getBoundingBox().
	 */
	Rectangle touchRect(DtlibInputTframe dtInputFrame){
		return new Rectangle(
			dtInputFrame.getBoundingBox().ul.x,
			dtInputFrame.getBoundingBox().ul.y,
			dtInputFrame.getBoundingBox().lr.x -
			dtInputFrame.getBoundingBox().ul.x,
			dtInputFrame.getBoundingBox().lr.y -
			dtInputFrame.getBoundingBox().ul.y);
	}
	
	/**
	 * Calculates the center of a rectangle, even when coordinates are negatives.
	 * @param rect The rectangle in question.
	 * @return The center of the rectangle, represented as Point
	 */
	Point calculateCenter(Rectangle rect){
		int x,y;
		
		if(rect.isEmpty()) x = 0;
		else if(rect.x >= 0) 
			x = rect.x + Math.abs(rect.width / 2);
		else x = rect.x - Math.abs(rect.width / 2);
		
		if(rect.isEmpty()) y = 0;
		else if(rect.y >= 0)
			y = rect.y + Math.abs(rect.height / 2);
		else y = rect.y - Math.abs(rect.height / 2);
		
		debug("Center calculated: x=" + x + ", y=" + y);
		
		return new Point(x,y);
	}
	
	/**
	 * Processes input when two fingers are touching the table.
	 * Result can be zooming when the distance between two fingers
	 * is altered or context menu summoning when both fingers 
	 * are moved in the same direction. This works for fingers above 
	 * each other, besides each other or anything in between.
	 * @param currentFrame The current input frame.
	 * @requires getTouchType(currentFrame) == TOUCH_TWO_FINGERS.
	 */
	private void processTwoFingers(DtlibInputTframe previousFrame, 
									DtlibInputTframe currentFrame){
		
		int ddt = 2; //Distance Difference Threshold. Can be very small for zooming.
		
		//Get previous touch points.
		Point[] previousPoints = getTouchPoints(previousFrame); 
		Point prev1 = previousPoints[0];
		Point prev2 = previousPoints[1];
		//debug("Previous points: " + prev1 + " and " + prev2);
		
		//Get current touch points.
		Point[] currentPoints = getTouchPoints(currentFrame);
		Point curr1 = currentPoints[0];
		Point curr2 = currentPoints[1];
		//debug("Current points: " + curr1 + " and " + curr2);
		
		//Calculate distance difference between frames. Used in zooming.
		double previousDistance = prev1.distance(prev2);
		double currentDistance = curr1.distance(curr2);
		double distanceDiff = currentDistance - previousDistance;
		
		//Only zoom when there is a real movement, and if the movement is not too sudden.
		debug("Distance diff: " + distanceDiff);
		if(Math.abs(distanceDiff) > ddt && Math.abs(distanceDiff) < 30){
			try{
				Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
				//Mouse moves up for zooming out (y becomes greater).
				//Mouse moves down for zooming in (y becomes smaller).
				robot.mouseMove(mouseLoc.x, mouseLoc.y + (int)distanceDiff/2);
				debug("Zooming from " + mouseLoc + " to <" + mouseLoc.x + "," + (mouseLoc.y + (int)distanceDiff/2) + ">");
			} catch(HeadlessException e) {e.printStackTrace();}

		} else{ //Check for rightclick.
			
			//Calculate average difference in x movement between frames.
			int xChange1 = prev1.x - curr1.x;
			int xChange2 = prev2.x - curr2.x;
			int avgXChange = (Math.abs(xChange1) + Math.abs(xChange2)) / 2;
			//debug("X change: " + xChange1 + " and " + xChange2);
			
			//Calculate average difference in y movement between frames.
			int yChange1 = prev1.y - curr1.y;
			int yChange2 = prev2.y - curr2.y;
			int avgYChange = (Math.abs(yChange1) + Math.abs(yChange2)) / 2;
			//debug("Y change: " + yChange1 + " and " + yChange2);
			
			//Check for menu gesture in x direction
			if(!rightClicked && 
					Math.abs(xChange1 - xChange2) < ddt &&
					Math.abs(xChange1) > ddt && 
					Math.abs(xChange2) > ddt &&
					sameSign(xChange1, xChange2) &&
					Math.abs(curr1.y - curr2.y) > ddt &&
					avgXChange > avgYChange &&
					avgYChange < ddt){
				//Both points are moving the same way, so rightclick.
				debug("X change rightclick, avgXChange is " + avgXChange + ", avYChange is " + avgYChange);
				stopGesture(MODE_ZOOMING_TWISTING);
				robot.mousePress(InputEvent.BUTTON3_MASK);
				robot.mouseRelease(InputEvent.BUTTON3_MASK);
				rightClicked = true;
			} // Check for menu gesture in y direction 
			else if(!rightClicked && 
					Math.abs(yChange1 - yChange2) < ddt &&
					Math.abs(yChange1) > ddt && 
					Math.abs(yChange2) > ddt &&
					sameSign(yChange1, yChange2) &&
					Math.abs(curr1.x - curr2.x) > ddt &&
					avgYChange > avgXChange &&
					avgXChange < ddt){
				//Both points are moving the same way, so rightclick.
				debug("Y change rightclick, avgXChange is " + avgXChange + ", avYChange is " + avgYChange);
				stopGesture(MODE_ZOOMING_TWISTING);
				robot.mouseMove(curr1.x, curr1.y);
				robot.mousePress(InputEvent.BUTTON3_MASK);
				robot.mouseRelease(InputEvent.BUTTON3_MASK);
				robot.mouseMove(curr1.x, curr1.y);
				rightClicked = true;
			}
		}
		
	}
	
	/**
	 * Constructs the two touch points from an inputframe with
	 * touch type TOUCH_TWO_FINGERS.
	 * @param frame The frame in question.
	 * @return An int[2] containing the points.
	 */
	private Point[] getTouchPoints(DtlibInputTframe frame){
		DtlibSegment[] xSegments = frame.getXSegments();
		DtlibSegment[] ySegments = frame.getYSegments();
		
		Point[] res = new Point[2];
		res[0] = new Point(-1,-1);
		res[1] = new Point(-1,-1);
		
		if(xSegments.length == 2 && ySegments.length == 1){
			//Two fingers next to each other ( . . ).
			
			//Construct the two points.
			res[0].x = getSegmentCenter(xSegments[0]);
			res[1].x = getSegmentCenter(xSegments[1]);
			int y = getSegmentCenter(ySegments[0]);
			res[0].y = y;
			res[1].y = y;
			
		} else if(xSegments.length == 1 && ySegments.length == 2){
			//Two fingers above each other ( : ).
			
			//Construct the two points.
			int x = getSegmentCenter(xSegments[0]);
			res[0].x = x;
			res[1].x = x;
			res[0].y = getSegmentCenter(ySegments[0]);
			res[1].y = getSegmentCenter(ySegments[1]);
			
		} else if(xSegments.length == 2 && ySegments.length == 2){
			//Two fingers at an angle ( . ' ) or ( ' . ).
			
			//Construct the two points.
			res[0].x = getSegmentCenter(xSegments[0]);
			res[1].x = getSegmentCenter(xSegments[1]);
			res[0].y = getSegmentCenter(ySegments[0]);
			res[1].y = getSegmentCenter(ySegments[1]);
			
		} else{
			//Some strange input.
			debug("Erroneous input for two fingers");
		}
		
		return res;
	}
	
	/**
	 * Calculates the center of a DtlibSegment in numbers relative to the screen.
	 * @param segment The segment in question.
	 * @return The screen coordinate of the center of the segment.
	 */
	private int getSegmentCenter(DtlibSegment segment){
		return (Math.abs(segment.startPos) + Math.abs(segment.stopPos))/2;
	}
	
	//=====================================
	//End gesture processing methods
    //=====================================
	
	//=====================================
	//Begin help methods
    //=====================================
	
	/**
	 * Determines if two numbers are both positive or both negative.
	 * @param first The first number.
	 * @param second The second number.
	 * @return Whether the numbers are both positive or both negative.
	 */
	private boolean sameSign(int first, int second){
		return (first >= 0 && second >= 0) || (first < 0 && second < 0);
	}
	
	/**
	 * Checks if anyone but the current user is navigating.
	 * @param currentUserID The user ID that should be excluded (usually the current user).
	 * @return If anyone but the current user is in a mode other than MODE_UNKNOWN.
	 */
	boolean isAnyoneNavigating(int currentUserID){
		for(int i = 0; i < 5; i++){
			if(i != currentUserID && currentMode[i] != MODE_UNKNOWN){
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Checks if the current user is allowed the turn.
	 * @param currentUserID The current user.
	 * @return Returns if the current user's touch can be processed.
	 */
	private boolean hasTurn(int currentUserID){
		switch(turntakingProtocol){
		case TT_NONE:
			//Make sure noone else is navigating.
			return !isAnyoneNavigating(currentUserID);
		case TT_PAUSE_DETECTION:
			long pause = System.currentTimeMillis() - touchReleaseTime;
			debug("Pause is " + pause);
			//debug("Previous user is " + previousUser);
			//Make sure noone else is navigating.

			return previousUser == -1 || 
				previousUser == currentUserID || 
				(previousUser != currentUserID && 
				!isAnyoneNavigating(currentUserID) && 
				pause > pauseDelay);
					
		case TT_MODERATOR:
			return currentTurn == currentUserID;
		case TT_EXPLICIT:
			return currentTurn == currentUserID;
		case TT_QUEUEMODERATOR:
			return currentTurn == currentUserID;
		default: 
			debug("Unknown turntaking protocol.");
			return true;
		}
	}
	
	/**
	 * Use this method to implement functional requests or queuing 
	 * for a turn. 
	 * @param user The user that requests the turn.
	 */
	public abstract void requestTurn(UserButton user);
	
	/**
	 * Use this method to implement the functionality to take a turn.
	 * @param user The user that takes the turn.
	 */
	public abstract void takeTurn(UserButton user);
	
	/**
	 * Use this method to implement turn release or loss functionality.
	 * @param user The user that releases the turn.
	 */
	public abstract void releaseTurn(UserButton user);
	
	/**
	 * Sends a script string to the jmol viewer.
	 * Scripting documentation at 
	 * <a href="http://chemapps.stolaf.edu/jmol/docs/">Jmol Interactive Script Documentation</a>
	 * @param script The script to send to the Jmol viewer
	 * @return jmolView.viewer.evalString(script)
	 */
    public String scriptInput(String script) {
		//debug("Script: " + script);
		return jmolView.viewer.evalString(script);
		
	}
    
    /**
     * Prints a debug message at the console.
     */
    public static void debug(Object message){
    	System.out.println("DEBUG: " + message);
    }
    
    //=====================================
	//End help methods
    //=====================================

    //=====================================
	//Begin DTTouchListener methods
    //=====================================
    
	public void tableDisconnected() {
        System.out.println(
                "DiamondTouch Table Disconnected, exiting application...");
        System.out.flush();
        System.exit(0);
    }
	
	/**
	 * This method is called by the DiamondTouch whenever a user starts a touch.
	 * In this method no gesture modes are set, but double tapping is done here. 
	 * It also handles fist touches in the bookmark/screenshot panel.
	 */
	public void touchDown(int userID, DtlibInputTframe dtInputFrame){
		
		debug("Touchdown user " + userID);

		touchLocation = determineLocation(userID, dtInputFrame);
		//molpanel and screenshot panel are handled here, handling of other
		//panels should be done in their implementation of determineLocation().
		if(touchLocation != LOCATION_MOLPANEL && touchLocation != LOCATION_SCREENSHOTPANEL){
			debug("Did not touch molpanel or bookmarks.");
			previousFrame[userID] = dtInputFrame;
			return;
		}

		//Check if the user is allowed to start touch.
		if(!hasTurn(userID)) {
			previousFrame[userID] = dtInputFrame;
			return;
		}
		
		//User has the turn, let's start.
		previousUser = userID;
		
		Point center = calculateCenter(touchRect(dtInputFrame));
    	centerX = center.x;
    	centerY = center.y;
    	robot.mouseMove(centerX,centerY);
		
		if(getTouchType(dtInputFrame) == TOUCH_ONE_FINGER){
			long current = System.currentTimeMillis();

			//The maybeDoubletap flag was not set properly previously, because the 
			//time threshold case was missing. Source of great headaches!
			if(!maybeDoubletap[userID] || current - previousTapTime[userID] > 700 ){
				// This certainly is no double tap, but might become one.
				previousTapTime[userID] = current;
				int xLoc = getSegmentCenter(dtInputFrame.getXSegments()[0]);
				int yLoc = getSegmentCenter(dtInputFrame.getYSegments()[0]);
				previousTapLoc[userID] = new Point(xLoc, yLoc);
				maybeDoubletap[userID] = true;
				//debug("Setting maybedoubletap flag, point is " + new Point(xLoc, yLoc));
			} else{
				
				// This is a potential double tap.
				maybeDoubletap[userID] = false;
				int xLocCurr = getSegmentCenter(dtInputFrame.getXSegments()[0]);
				int yLocCurr = getSegmentCenter(dtInputFrame.getYSegments()[0]);
				int xLocPrev = previousTapLoc[userID].x;
				int yLocPrev = previousTapLoc[userID].y;
				//debug("This may be a doubletap, point is " + new Point(xLocCurr, yLocCurr));
				if(Math.abs(xLocCurr - xLocPrev) < FINGER_SIZE &&
					Math.abs(yLocCurr - yLocPrev) < FINGER_SIZE &&
					current - previousTapTime[userID] < 700){ //This is the time threshold.
					//Double tap detected.
					if(touchLocation == LOCATION_MOLPANEL){
						//Doubletap on molpanel means center picking.
						//debug("Molpanel doubletap.");
						//TODO Test alternative: script "zoom (atom expression) or {x y z} 0}"
						scriptInput("set picking CENTER");
						robot.mousePress(InputEvent.BUTTON1_MASK);
						robot.mouseRelease(InputEvent.BUTTON1_MASK);
						scriptInput("set picking OFF");
						doubleTap = true;
					} else if(touchLocation == LOCATION_SCREENSHOTPANEL){
						//Doubletap on screenshot panel means screenshot recall.
						//debug("Screenshot panel doubletap.");
						robot.mousePress(InputEvent.BUTTON3_MASK);
						robot.mouseRelease(InputEvent.BUTTON3_MASK);
						doubleTap = true;
					}
				}
				//else debug("No double tap, timediff is " + (current - previousTapTime[userID]));
			}
		} else if(touchLocation == LOCATION_SCREENSHOTPANEL &&
				getTouchType(dtInputFrame) == TOUCH_NON_FINGER &&
				singleNonFingerSegment(dtInputFrame)){
			//A fist was planted on the screenshot panel, lets see what to do.
			Screenshot shot = touchedScreenshot();
			if(shot != null){
				//We touched a screenshot, lets remove it.
				robot.mousePress(InputEvent.BUTTON2_MASK);
				robot.mouseRelease(InputEvent.BUTTON2_MASK);
			} else{
				//We hit an empty space, lets add a shot.
				Point s = screens.getLocationOnScreen();
				Point p = new Point(centerX-s.x, centerY-s.y);
				screens.addScreenshot(jmolView.getScreenshot(), 
						jmolView.viewer.getStateInfo(), this, jmolView.zoom, p);
			}
		}else{
			//No possible double tap, proceed as normal.
		}
		//Save the current bounding box for history tracking. 
		previousFrame[userID] = dtInputFrame;
	}
	
	/**
	 * This method is called each frame while the table is detection touch.
	 * If a touch is really fast (touch & release instantly), this method is not called.
	 * It processes all gesture input, except for double tapping.
	 */
	public void touchDuring(int userID, DtlibInputTframe dtInputFrame){
		
		debug("Touchduring user " + userID);
		
		touchLocation = determineLocation(userID, dtInputFrame);
		//molpanel and screenshot panel are handled here, handling of other
		//panels should be done in their implementation of determineLocation().
		if(touchLocation != LOCATION_MOLPANEL && touchLocation != LOCATION_SCREENSHOTPANEL){
			debug("Touched molpanel or bookmarks.");
			previousFrame[userID] = dtInputFrame;
			return;
		}
		
		//Check if the user is allowed to navigate first.
		if(!hasTurn(userID)) {
			previousFrame[userID] = dtInputFrame;
			return;
		}
		
		//If a double tap is occurring, we don't need to process more,
		//the touch will be released soon.
		if(doubleTap) return;
		
		int sct = 30; //Size Change Threshold
		
    	Rectangle currentTouchRect = touchRect(dtInputFrame);
    	Rectangle previousTouchRect = touchRect(previousFrame[userID]);
    	Point center = calculateCenter(currentTouchRect);
    	centerX = center.x;
    	centerY = center.y;
    	Point prevCenter = calculateCenter(previousTouchRect);
    	
    	int currentTouch = getTouchType(dtInputFrame);
    	
		switch(currentMode[userID]){
		case MODE_UNKNOWN:
			//There can't be a context menu, so we can disable right click flag.
			rightClicked = false;
			//Assign the current mode, depending on the type of touch.
			switch(currentTouch){
			case TOUCH_ONE_FINGER:
				debug("Rotation start.");
				//We are beginning rotation.
				currentMode[userID] = MODE_ROTATING;
				startGesture(MODE_ROTATING);
				break;
			case TOUCH_TWO_FINGERS:
				currentMode[userID] = MODE_ZOOMING_TWISTING;
				startGesture(MODE_ZOOMING_TWISTING);
				break;
			case TOUCH_MORE_FINGERS:
				//We are starting translation.
				currentMode[userID] = MODE_TRANSLATING;
				startGesture(MODE_TRANSLATING);
				break;
			case TOUCH_NON_FINGER:
				if(singleNonFingerSegment(dtInputFrame)){
					//Fist on the table, reset.
					//debug("Fist detected");
					if(touchLocation == LOCATION_MOLPANEL)
						//Fist on molpanel means molecule reset.
						resetMolecule();
				}
				else{
					//Hand on the table
					//debug("Hand detected on the table.");
					currentMode[userID] = MODE_TRANSLATING;
					startGesture(MODE_TRANSLATING);
				}
				break;
			default: debug("Dunno what the mode is for touch " + currentTouch);
			}
			
			break;
		case MODE_ROTATING:
			if(currentTouch == TOUCH_TWO_FINGERS){
				//An extra finger has been detected, should be zooming/twisting.
				currentMode[userID] = MODE_ZOOMING_TWISTING;
				stopGesture(MODE_ROTATING);
				startGesture(MODE_ZOOMING_TWISTING);
			}else{
				if(Math.abs(centerX-prevCenter.x) < sct && Math.abs(centerY-prevCenter.y) < sct){
					//The rotation has already started, let's rotate some.
					robot.mouseMove(centerX, centerY);
				}else{
					stopGesture(MODE_ROTATING);
					robot.mouseMove(centerX, centerY);
					startGesture(MODE_ROTATING);
				}
			}
			break;
		case MODE_ZOOMING_TWISTING:
			if(currentTouch == TOUCH_ONE_FINGER){
				//A finger has been released, should be rotating.
				currentMode[userID] = MODE_ROTATING;
				stopGesture(MODE_ZOOMING_TWISTING);
				startGesture(MODE_ROTATING);
			}else if(currentTouch == TOUCH_MORE_FINGERS){
				//Extra finger detected, should be translating.
				currentMode[userID] = MODE_TRANSLATING;
				stopGesture(MODE_ZOOMING_TWISTING);
				startGesture(MODE_TRANSLATING);
			}else{
				//Too much code to put in here.
				processTwoFingers(previousFrame[userID],dtInputFrame);
			}
				
			break;
		case MODE_TRANSLATING:
			//If the touch rectangle changes suddenly, a finger has been released.
			//Translation should be reevaluated.
			if(Math.abs(currentTouchRect.width - previousTouchRect.width) > sct ||
					Math.abs(currentTouchRect.height - previousTouchRect.height) > sct){
				//Stop the gesture, and let the next frame reevaluate.
				stopGesture(MODE_TRANSLATING);
				currentMode[userID] = MODE_UNKNOWN;
			}
			else{
				//We have already started translation, let's do some more.
				robot.mouseMove(centerX, centerY);
			}
			
			break;
		//The default case should not happen.
		default:
			debug("Unknown user mode");
		}
		debug("Current touch mode: " + currentMode[userID]);
    	//Store the current bounding box for history tracking.
		previousFrame[userID] = dtInputFrame;
		
	}
	
	/**
	 * This method is called when the table registers stops registering a touch
	 * The current touch mode of the user is reset to MODE_UNKNOWN, and any
	 * keys or buttons that were still pressed are released.
	 */
	public void touchReleased(int userID, DtlibInputTframe dtInputFrame){
		
		debug("Touchrelease user " + userID);
		
		if(!hasTurn(userID)) return;
		
		doubleTap = false;
		stopGesture(currentMode[userID]);
		currentMode[userID] = MODE_UNKNOWN;
		touchReleaseTime = System.currentTimeMillis();
	}
	
	//=====================================
	//End DTTouchListener methods
    //=====================================
	
	//=====================================
	//Begin PropertyChangeListener methods
    //=====================================
	
	public void propertyChange(PropertyChangeEvent e) {
		if (e.getPropertyName().equals(Screenshot.PROPERTY_RESTORE_SCREENSHOT)) {
			jmolView.restoreScreenshot((Screenshot) e.getNewValue());
		}
	}
	
	//=====================================
	//End PropertyChangeListener methods
    //=====================================
	
	//=====================================
	//Begin KeyListener methods
    //=====================================
	
	public void keyTyped(KeyEvent e) {}

	public void keyPressed(KeyEvent e) {}

	/**
	 * Processes key events. Currently supports the following: <br>
	 * (p) Make screenshot <br>
	 * (s) Toggle stereo view <br>
	 * (r) Reset the molecule <br>
	 * (1) Load pdb oxycodone molecule <br>
	 * (2) Load pdb 1a3n molecule (hemoglobine) <br>
	 * (3) Load pdb 1amk molecule <br>
	 * (4) Load pdb 1oy3 molecule (p65) <br>
	 * (5) Load pdb 1e3g molecule (androgen receptor) <br>
	 * (6) Load pdb 2mrn molecule (p63) <br>
	 * (7) Load pdb 6pax molecule (pax-6 dna) <br>
	 * (ESC) Exit the application <br>
	 */
	public void keyReleased(KeyEvent e) {
		switch(e.getKeyChar()){
		case 'p': BufferedImage shot = jmolView.getScreenshot();
			Point p = new Point((int)(shot.getWidth()*Screenshot.FACTOR_WIDTH), 
					(int)(shot.getHeight()*Screenshot.FACTOR_HEIGHT));
			screens.addScreenshot(shot,	jmolView.viewer.getStateInfo(),	
					this, jmolView.zoom, p); 
			break;
		case 's': jmolView.toggleStereo(); break;
		case 'r': resetMolecule(); break;
		case 't': jmolView.displayText("Display test"); break;
		case 'z': String z = scriptInput("show ZOOM"); 
				debug("Current zoom: " + z); break;
		case '1': jmolView.loadPDB(pdboxycodone); break;
		case '2': jmolView.loadPDB(pdb1a3n); break;
		case '3': jmolView.loadPDB(pdb1amk); break;
		case '4': jmolView.loadPDB(pdb1oy3); break;
		//case '5': jmolView.evalString("load " + molaspartame); break;
		case '5': jmolView.loadPDB(pdb1e3g); break;
		case '6': jmolView.loadPDB(pdb2rmn); break;
		case '7': jmolView.loadPDB(pdb6pax); break;
		}
		
		switch(e.getKeyCode()){
		case KeyEvent.VK_ESCAPE:
			System.exit(0);
			break;
		}
			
	}
	
	//=====================================
	//End KeyListener methods
    //=====================================
	
	//=====================================
	//Begin JmolStatusListener methods
    //=====================================
	
	@Override
	public String eval(String arg0) {return null;}

	@Override
	public float[][] functionXY(String arg0, int arg1, int arg2) {return null;}

	@Override
	/**
	 * Calls the popup menu at the spot where you want it to.
	 */
	public void handlePopupMenu(int x, int y) {
		if ( jmolView.popup != null) {
			jmolView.popup.show(x,y);
		}
		jmolView.popup.updateComputedMenus();
		
	}

	@Override
	public String createImage(String arg0, String arg1, Object arg2, int arg3) {return null;}

	@Override
	public String dialogAsk(String arg0, String arg1) {return null;}

	@SuppressWarnings("unchecked")
	@Override
	public Hashtable getRegistryInfo() {return null;}

	@Override
	public void notifyCallback(int arg0, Object[] arg1) {
		if(arg0 == 1)
		debug("Resize callback: " + arg1[1]);
		//TODO Tokenize zoom string, store somewhere.
	}

	@Override
	public boolean notifyEnabled(int arg0) {return false;}

	@Override
	public void setCallbackFunction(String callbackType, String callbackFunction) {
		debug("Callback! " + callbackType + " and " + callbackFunction);
	}

	@Override
	public void showConsole(boolean arg0) {}

	@Override
	public void showUrl(String arg0) {}
	
	//=====================================
	//End JmolStatusListener methods
    //=====================================

	//=====================================
	//Begin FocusListener methods
    //=====================================
	
	@Override
	public void focusGained(FocusEvent arg0) {}

	@Override
	/**
	 * This override method requests focus, this is is necessary 
	 * to regain focus after exiting the context menu. Without it,
	 * the KeyListener stops working.
	 */
	public void focusLost(FocusEvent arg0) {
		requestFocus();
	}
	
	//=====================================
	//End FocusListener methods
    //=====================================
}
